import sys
import os
import multiprocessing as MulProcess
import datetime

from common.benchmark_var import BenchAction, BenchMode, ReportType, StatusCode, \
    ConfigSections as Sections, ConfigOptions as Options
from common.logger import log
from utils.utils import ErrorHandler


class OptionValidator:
    def __init__(self, options):
        self.options = options
        self.default_validators = {
            Options.ACTION: self.verify_list(value_list=[BenchAction.STANDARD, BenchAction.RUN,
                                                         BenchAction.BUILD, BenchAction.CLEAN]),
            Options.MODE: self.verify_list(value_list=[BenchMode.SERVER, BenchMode.DESKTOP, BenchMode.LAPTOP, BenchMode.TSSERVER, BenchMode.TSDESKTOP, BenchMode.DEFAULT]),
            Options.TEST_TARGET: self.verify_target,
            Options.ITERATIONS: self.verify_num(minvalue=1),
            Options.COPIES: self.verify_num(minvalue=1),
            Options.REPORT: self.verify_list(value_list=[ReportType.ALL, ReportType.CSV,
                                                         ReportType.PDF, ReportType.TXT]),
            Options.OUTPUT_DIR: self.verify_dir,
            Options.SKIP_FAIL: self.verify_boolean,
            Options.SKIP_VERIFY: self.verify_boolean,
            Options.AUTO_UPLOAD: self.verify_boolean,
            Options.DEBUG: self.verify_boolean,
            Options.TASKSET: self.verify_taskset(self.options.get(Sections.GENERAL).get('bind_cores'),
                                                 self.options.get(Sections.GENERAL).get('bind_copies')),
            Options.BIND_COPIES: self.verify_bind_copies(self.options.get(Sections.GENERAL).get('copies'),
                                                     self.options.get(Sections.GENERAL).get('bind_cores')),
            Options.BIND_CORES: self.verify_bind_cores(self.options.get(Sections.GENERAL).get('bind_copies')),

            Options.MANUFACTURER: self.verify_str,
            Options.PRODUCT_MODEL: self.verify_str,
            Options.CPU_MODEL: self.verify_str,
            Options.NETCARDS: self.verify_str,
            Options.RAID: self.verify_str,
            Options.DISPLAY_CARDS: self.verify_str,
            Options.HARDWARE_OTHERS: self.verify_str,
            Options.OS: None,
            Options.SOFTWARE_OTHERS: self.verify_str,
            Options.TESTER_NAME: self.verify_str,
            Options.TEST_DATE: self.verify_date,

            Options.BASE_DIR: None,
            Options.LD_LIBRARY_PATH: None,
            Options.LD_FLAGS: self.verify_flags('-D'),

            Options.CC: self.verify_compiler,
            Options.CC_VERSION: None,
            Options.CFLAGS: self.verify_flags('-D'),

            Options.CLD: self.verify_flags('-D'),
            Options.CLD_FLAGS: self.verify_flags('-D'),

            Options.CXX: self.verify_compiler,
            Options.CXX_VERSION: None,
            Options.CXXFLAGS: self.verify_flags('-D'),

            Options.CXXLD: self.verify_flags('-D'),
            Options.CXXLD_FLAGS: self.verify_flags('-D'),

            Options.JAVA_OPTIONS: self.verify_flags('-Xmx'),

            Options.LIBS: None,
            Options.AR: None,
            Options.RANLIB: None
        }

    @staticmethod
    def verify_num(minvalue=0, maxvalue=sys.maxsize):
        def _verify_num(value):
            if minvalue <= value <= maxvalue:
                return True
            return False
        return _verify_num

    @staticmethod
    def verify_taskset(bind_cores, bind_copies, maxvalue=MulProcess.cpu_count() - 1):
        def _verify_taskset(value):
            if not isinstance(value, list):
                return False
            if value and (bind_cores or bind_copies):
                log.error("Invalid parameter: taskset and numa set cannot be set at the same time")
                return False
            for val in value:
                if OptionValidator.verify_num(maxvalue=maxvalue)(val):
                    continue
                else:
                    return False
            return True
        return _verify_taskset

    @staticmethod
    def verify_bind_copies(copies, bind_cores, maxvalue=MulProcess.cpu_count() - 1):
        def _verify_bind_copies(value):
            if not isinstance(value, list):
                return False
            if value and not bind_cores:
                log.error("Invalid parameter: bind_cores and bind_copies must be set simultaneously")
                return False
            if isinstance(bind_cores, list):
                if len(value) > len(bind_cores):
                    log.error("Invalid parameter: "
                              "The number of bind_cores must be greater than the number of bind_copies")
                    return False
            for val_list in value:
                for val in val_list:
                    if 0 <= val <= maxvalue:
                        continue
                    else:
                        return False
            return True

        return _verify_bind_copies

    @staticmethod
    def verify_bind_cores(bind_copies, maxvalue=MulProcess.cpu_count() - 1):
        def _verify_bind_cores(value):
            if not isinstance(value, list):
                return False
            if value and not bind_copies:
                log.error("Invalid parameter: bind_cores and bind_copies must be set simultaneously")
                return False
            for val_list in value:
                for val in val_list:
                    if 0 <= val <= maxvalue:
                        continue
                    else:
                        return False

            return True

        return _verify_bind_cores

    @staticmethod
    def verify_boolean(value):
        if value in (0, 1):
            return True
        return False

    @staticmethod
    def verify_str(value):
        if not isinstance(value, str):
            return False
        return True

    @staticmethod
    def verify_list(value_list):
        def _verify_list(value):
            if isinstance(value, list):
                for val in value:
                    return _verify_list(val)

            if value not in value_list:
                return False
            return True
        return _verify_list

    @staticmethod
    def verify_date(value):
        try:
            datetime.datetime.strptime(value, '%Y.%m.%d')
            return True
        except ValueError:
            return False

    @staticmethod
    def verify_dir(value):
        current_path = os.getcwd()
        path = os.path.join(current_path, '..', value)
        if not os.path.exists(path):
            os.makedirs(path)
        return True

    @staticmethod
    def verify_compiler(value):
        value = os.path.realpath(value)
        if os.path.isfile(value) and os.access(value, os.X_OK):
            return True
        return False

    @staticmethod
    def verify_flags(flags):
        def _verify_flags(value):
            if value.startswith(flags):
                return False
            return True
        return _verify_flags

    @staticmethod
    def verify_target(target_list):
        have_suite = False
        have_workload = False
        for target in target_list:
            if target.endswith("Suite"):
                have_suite = True
            else:
                have_workload = True
        if have_workload and have_suite:
            log.error("Error: Workload and Suite cannot be tested at the same time")
            return False

        return True


class OptionCombiner:
    """
    Merge options from configuration file and Command line.
    """

    def __init__(self):
        self.default_values = {
            # general options
            Options.ACTION: (Sections.GENERAL, 'standard'),
            Options.MODE: (Sections.GENERAL, "Server"),
            Options.TEST_TARGET: (Sections.GENERAL, []),
            Options.ITERATIONS: (Sections.GENERAL, 1),
            Options.COPIES: (Sections.GENERAL, 1),
            Options.REPORT: (Sections.GENERAL, ['all']),
            Options.OUTPUT_DIR: (Sections.GENERAL, 'output_dir'),
            Options.SKIP_FAIL: (Sections.GENERAL, 0),
            Options.SKIP_VERIFY: (Sections.GENERAL, 0),
            Options.AUTO_UPLOAD: (Sections.GENERAL, 0),
            Options.DEBUG: (Sections.GENERAL, 0),
            Options.TASKSET: (Sections.GENERAL, []),
            Options.BIND_COPIES: (Sections.GENERAL, []),
            Options.BIND_CORES: (Sections.GENERAL, []),
            # optional options
            Options.MANUFACTURER: (Sections.GENERAL, ''),
            Options.PRODUCT_MODEL: (Sections.GENERAL, ''),
            Options.CPU_MODEL: (Sections.GENERAL, ''),
            Options.NETCARDS: (Sections.GENERAL, ''),
            Options.RAID: (Sections.GENERAL, ''),
            Options.DISPLAY_CARDS: (Sections.GENERAL, ''),
            Options.HARDWARE_OTHERS: (Sections.GENERAL, ''),
            Options.OS: (Sections.GENERAL, ''),
            Options.SOFTWARE_OTHERS: (Sections.GENERAL, ''),
            Options.TESTER_NAME: (Sections.GENERAL, ''),
            Options.TEST_DATE: (Sections.GENERAL, datetime.datetime.now().strftime('%Y.%m.%d')),

            # namespace & compiler options
            Options.BASE_DIR: (Sections.COMPILER, '/usr/bin'),
            Options.LD_LIBRARY_PATH: (Sections.COMPILER, ''),
            Options.LD_FLAGS: (Sections.COMPILER, ''),

            Options.CC: (Sections.COMPILER, '/usr/bin/gcc'),
            Options.CC_VERSION: (Sections.COMPILER, '-v'),
            Options.CFLAGS: (Sections.COMPILER, ''),

            Options.CLD: (Sections.COMPILER, ''),
            Options.CLD_FLAGS: (Sections.COMPILER, ''),

            Options.CXX: (Sections.COMPILER, '/usr/bin/g++'),
            Options.CXX_VERSION: (Sections.COMPILER, '-v'),
            Options.CXXFLAGS: (Sections.COMPILER, ''),

            Options.CXXLD: (Sections.COMPILER, ''),
            Options.CXXLD_FLAGS: (Sections.COMPILER, ''),

            Options.JAVA_OPTIONS: (Sections.COMPILER, ''),

            Options.LIBS: (Sections.COMPILER, ''),
            Options.AR: (Sections.COMPILER, 'ar'),
            Options.RANLIB: (Sections.COMPILER, 'ranlib'),
        }

    def combine_options(self, conf_option, cmd_option):
        options = conf_option
        # combine settings
        section = Sections.GENERAL
        for option in self.default_values.keys():
            option_section, _ = self.default_values.get(option)
            if option_section != section:
                continue

            option_value = self._final_option(option, conf_option[section], cmd_option)
            if option == Options.OUTPUT_DIR:
                option_value = self.get_output_dir(option_value)
            options[section][option] = option_value
        
        option_validators = OptionValidator(options)
        self.verify_options(options, option_validators)

        return options

    def _final_option(self, option, conf_option, cmd_option):
        if cmd_option.get(option) or isinstance(cmd_option.get(option), int):
            return cmd_option.get(option)
        
        if conf_option.get(option) or isinstance(conf_option.get(option), int):
            return conf_option.get(option)

        _, default_value = self.default_values.get(option)
        return default_value

    def verify_options(self, options, option_validators):
        # validate config file options
        if isinstance(options, dict):
            for section in options.keys():
                if isinstance(options[section], dict):
                    self.verify_options(options[section], option_validators)

                if not self.default_values.get(section):
                    continue
                option = options.get(section)
                validator = option_validators.default_validators[section]
                self._verify_val(option, section, validator)
    
    def _verify_val(self, option, section, validator):
        if callable(validator):
            if not validator(option):
                log.info("Invalid parameter: {}, invalid value: {}".format(section, option))
                ErrorHandler.check_error(StatusCode.INVALID_TYPE)

    @staticmethod
    def get_output_dir(output_dir):
        output_dir = os.path.abspath(output_dir)
        if not os.path.exists(output_dir):
            os.makedirs(output_dir)
        return output_dir
